package dynblock

import (
    "fmt"

    "Havoc/pkg/profile/yaotl"
    "github.com/zclconf/go-cty/cty"
    "github.com/zclconf/go-cty/cty/convert"
)

type expandSpec struct {
    blockType      string
    blockTypeRange hcl.Range
    defRange       hcl.Range
    forEachVal     cty.Value
    iteratorName   string
    labelExprs     []hcl.Expression
    contentBody    hcl.Body
    inherited      map[string]*iteration
}

func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Block) (*expandSpec, hcl.Diagnostics) {
    var diags hcl.Diagnostics

    var schema *hcl.BodySchema
    if len(blockS.LabelNames) != 0 {
        schema = dynamicBlockBodySchemaLabels
    } else {
        schema = dynamicBlockBodySchemaNoLabels
    }

    specContent, specDiags := rawSpec.Body.Content(schema)
    diags = append(diags, specDiags...)
    if specDiags.HasErrors() {
        return nil, diags
    }

    //// for_each attribute

    eachAttr := specContent.Attributes["for_each"]
    eachVal, eachDiags := eachAttr.Expr.Value(b.forEachCtx)
    diags = append(diags, eachDiags...)

    if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType {
        // We skip this error for DynamicPseudoType because that means we either
        // have a null (which is checked immediately below) or an unknown
        // (which is handled in the expandBody Content methods).
        diags = append(diags, &hcl.Diagnostic{
            Severity:    hcl.DiagError,
            Summary:     "Invalid dynamic for_each value",
            Detail:      fmt.Sprintf("Cannot use a %s value in for_each. An iterable collection is required.", eachVal.Type().FriendlyName()),
            Subject:     eachAttr.Expr.Range().Ptr(),
            Expression:  eachAttr.Expr,
            EvalContext: b.forEachCtx,
        })
        return nil, diags
    }
    if eachVal.IsNull() {
        diags = append(diags, &hcl.Diagnostic{
            Severity:    hcl.DiagError,
            Summary:     "Invalid dynamic for_each value",
            Detail:      "Cannot use a null value in for_each.",
            Subject:     eachAttr.Expr.Range().Ptr(),
            Expression:  eachAttr.Expr,
            EvalContext: b.forEachCtx,
        })
        return nil, diags
    }

    //// iterator attribute

    iteratorName := blockS.Type
    if iteratorAttr := specContent.Attributes["iterator"]; iteratorAttr != nil {
        itTraversal, itDiags := hcl.AbsTraversalForExpr(iteratorAttr.Expr)
        diags = append(diags, itDiags...)
        if itDiags.HasErrors() {
            return nil, diags
        }

        if len(itTraversal) != 1 {
            diags = append(diags, &hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Invalid dynamic iterator name",
                Detail:   "Dynamic iterator must be a single variable name.",
                Subject:  itTraversal.SourceRange().Ptr(),
            })
            return nil, diags
        }

        iteratorName = itTraversal.RootName()
    }

    var labelExprs []hcl.Expression
    if labelsAttr := specContent.Attributes["labels"]; labelsAttr != nil {
        var labelDiags hcl.Diagnostics
        labelExprs, labelDiags = hcl.ExprList(labelsAttr.Expr)
        diags = append(diags, labelDiags...)
        if labelDiags.HasErrors() {
            return nil, diags
        }

        if len(labelExprs) > len(blockS.LabelNames) {
            diags = append(diags, &hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Extraneous dynamic block label",
                Detail:   fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)),
                Subject:  labelExprs[len(blockS.LabelNames)].Range().Ptr(),
            })
            return nil, diags
        } else if len(labelExprs) < len(blockS.LabelNames) {
            diags = append(diags, &hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Insufficient dynamic block labels",
                Detail:   fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)),
                Subject:  labelsAttr.Expr.Range().Ptr(),
            })
            return nil, diags
        }
    }

    // Since our schema requests only blocks of type "content", we can assume
    // that all entries in specContent.Blocks are content blocks.
    if len(specContent.Blocks) == 0 {
        diags = append(diags, &hcl.Diagnostic{
            Severity: hcl.DiagError,
            Summary:  "Missing dynamic content block",
            Detail:   "A dynamic block must have a nested block of type \"content\" to describe the body of each generated block.",
            Subject:  &specContent.MissingItemRange,
        })
        return nil, diags
    }
    if len(specContent.Blocks) > 1 {
        diags = append(diags, &hcl.Diagnostic{
            Severity: hcl.DiagError,
            Summary:  "Extraneous dynamic content block",
            Detail:   "Only one nested content block is allowed for each dynamic block.",
            Subject:  &specContent.Blocks[1].DefRange,
        })
        return nil, diags
    }

    return &expandSpec{
        blockType:      blockS.Type,
        blockTypeRange: rawSpec.LabelRanges[0],
        defRange:       rawSpec.DefRange,
        forEachVal:     eachVal,
        iteratorName:   iteratorName,
        labelExprs:     labelExprs,
        contentBody:    specContent.Blocks[0].Body,
    }, diags
}

func (s *expandSpec) newBlock(i *iteration, ctx *hcl.EvalContext) (*hcl.Block, hcl.Diagnostics) {
    var diags hcl.Diagnostics
    var labels []string
    var labelRanges []hcl.Range
    lCtx := i.EvalContext(ctx)
    for _, labelExpr := range s.labelExprs {
        labelVal, labelDiags := labelExpr.Value(lCtx)
        diags = append(diags, labelDiags...)
        if labelDiags.HasErrors() {
            return nil, diags
        }

        var convErr error
        labelVal, convErr = convert.Convert(labelVal, cty.String)
        if convErr != nil {
            diags = append(diags, &hcl.Diagnostic{
                Severity:    hcl.DiagError,
                Summary:     "Invalid dynamic block label",
                Detail:      fmt.Sprintf("Cannot use this value as a dynamic block label: %s.", convErr),
                Subject:     labelExpr.Range().Ptr(),
                Expression:  labelExpr,
                EvalContext: lCtx,
            })
            return nil, diags
        }
        if labelVal.IsNull() {
            diags = append(diags, &hcl.Diagnostic{
                Severity:    hcl.DiagError,
                Summary:     "Invalid dynamic block label",
                Detail:      "Cannot use a null value as a dynamic block label.",
                Subject:     labelExpr.Range().Ptr(),
                Expression:  labelExpr,
                EvalContext: lCtx,
            })
            return nil, diags
        }
        if !labelVal.IsKnown() {
            diags = append(diags, &hcl.Diagnostic{
                Severity:    hcl.DiagError,
                Summary:     "Invalid dynamic block label",
                Detail:      "This value is not yet known. Dynamic block labels must be immediately-known values.",
                Subject:     labelExpr.Range().Ptr(),
                Expression:  labelExpr,
                EvalContext: lCtx,
            })
            return nil, diags
        }

        labels = append(labels, labelVal.AsString())
        labelRanges = append(labelRanges, labelExpr.Range())
    }

    block := &hcl.Block{
        Type:        s.blockType,
        TypeRange:   s.blockTypeRange,
        Labels:      labels,
        LabelRanges: labelRanges,
        DefRange:    s.defRange,
        Body:        s.contentBody,
    }

    return block, diags
}
